feat(uxcore): dark mode across UX Core + layout/context fixes#114
Conversation
- /uxcore/[slug] now fetches UXCG questions in getStaticProps and feeds them via prop; the old read-from-GlobalContext path saw the null default (provider wires uxcgLocalizedData: null in _app.tsx) and the modal silently dropped its "This bias answers the following questions" section. - UXCG stage selector: invert + dim the unselected light-grey tile image in dark mode (selected tiles untouched, keep their colored bg). - UXCG PanelHeader: invert dark monochrome icons in dark mode so they read against the dark panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- LanguageSwitcher: dark dropdown bg + light link/hover so the panel stops leaking light-theme look inside the bias modal; flag SVGs now show on a matching dark surface. - Table show-more/less buttons: explicit dark border-color so the light default 1px border stops drawing a frame against the dark panel. - Input clear icon: invert filter so the dark monochrome × is visible against the dark input. - UserDropdown "Log In" / username: lift base text from rgba(0,0,0,.85) to the dark-theme foreground; same for hover/active. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
usePathname() from next/navigation is App-Router-only. In a Pages Router app it returns undefined during SSR and the real path on the client, so every conditional that branches on it (isUXCoreRoot, isUXCoreNested, shouldHideToolHeader) flipped between server and client render and produced a hydration mismatch on /uxcore (and any UX Core route). Swap to router.pathname which is identical in both phases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
router.asPath includes the URL hash on the client but is hash-free during SSR. Four render-time comparisons against asPath === '/uxcore' (or /uxcg) were flipping between server and client whenever the URL carried a hash (e.g. /uxcore#hr), producing a second hydration mismatch even after the previous usePathname() fix. - MobileHeader: gate the podcast button on pathname instead of asPath - ToolHeader: same for the two header tooltips and the desktop podcast button; introduce a local pathname constant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two sidebar switchers on /uxcore relied on a dataset.type === defaultViewLabel comparison that silently no-op'd whenever defaultViewLabel was undefined — which is exactly how UXCoreLayout calls the "View type" switcher (no defaultViewLabel prop). On a fresh load, the only ever-clickable button was the one whose state was already active, so neither button toggled. Drop the dataset trick: each button knows whether it's the first or second slot and toggles only when clicked from the opposite side. Also guard handleSnackbarOpening so the Use-cases pair stops throwing when the "View type" pair (no snackbar prop) is clicked. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review on #113: the UX Core font declarations + the scoped body.uxcorePage default no longer live in a sibling stylesheet. The @font-face block and the body.uxcorePage Lato fallback move into src/styles/globals.scss, the separate src/uxcore/styles/uxcore-fonts.scss is deleted, and the extra import in _app.tsx is dropped. The body.uxcorePage rule stays placed AFTER the global `*` selector so specificity still wins over the Source-Serif default. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- LanguageSwitcher: explicitly null any inherited image filter on the flag SVGs in dark mode so the colour stripes survive when the switcher sits inside a modal header whose dark rules apply saturate(0) / invert to images. - Tooltip popup: dark surface + arrow recolour for the non-Dark variant so the AnswerBiasLink hover popup inside the UXCG modal stops rendering as a white card on a dark modal. - BiasPopupContent: lift link colour and muted tip line for the dark surface. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The dark override applied the dark-monochrome invert/brightness filter to every navbar SVG, including the selected (Active / ActivePodcast / ActiveProjects) items whose base path fill is already #fafafa. On hover, the filter combined with the lighter hover background flattened the icon to dark grey on dark grey — effectively disappearing. Null the filter on selected variants (and their hover state) so the white fill wins. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two compounding bugs were silencing the right-sidebar switchers: 1. .coreView (the hexagon/spider page) is sized 100vw x 100vh and is rendered AFTER the two .viewTypeSwitcher / .viewTeamSwitcher siblings in the DOM. Without a z-index, the core view layer stacks on top of the absolute-positioned switchers and absorbs every click that lands on them. Lift both switchers to z-index: 3. 2. The earlier "click any inactive button toggles" refactor had the active/inactive sides inverted. Restore the correct semantics: first slot is active when isSecondView=true (FolderView class), second slot is active when isSecondView=false (CoreView class). Toggle fires only from the inactive side. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inactive stage tag pills sat at 0.3 opacity, which disappears against the dark page background — including the "All Questions" button whose inline #282828 bg matched the page bg almost exactly. Lift inactive pills to 0.75 with a faint border and re-skin "All Questions" to a lighter neutral so the row reads as a real filter strip in dark theme. Also capitalize the label to "All Questions" for consistency with the other pill labels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ut shift Dark theme for the shared Modal chrome (bg, header divider, title, close icon), the OurProjectsModal rows / GitHub-API outline buttons / divider, the LogInModal title / description / provider buttons, and the neutral Button variant (so the Cancel button reads on dark). Primary / Orange / BlueOutline button variants are left alone since they keep their brand colors. Also fixes the long-standing page-shift-on-modal-open: the old .hide-body-move rule added margin-right: 8px to <body> when a modal opened, which only partially compensated for the ~15px scrollbar (net visible jump). Switch to scrollbar-gutter: stable on <html>, which reserves the scrollbar's space at all times so toggling overflow:hidden on modal open no longer reflows the page sideways. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dark theme for the Contact Us modal (CustomModal chrome, body copy and inline links, Input + Textarea fields). Input + Textarea now flip globally on dark, which fixes other forms that reuse them too. Cross-realm dark-theme sync: keepsimple-side and UX Core-side each ship their own copy of useGlobals with separate module state. Both write to the same localStorage key and the same body class, so persistence was fine — but in-memory state diverged when the user toggled in one realm and navigated to the other, leaving the dark/light toggle button out of phase with the actual page. Wire a window 'darktheme:change' custom event that every toggle dispatches and that both realms subscribe to, so the realm you didn't toggle in mirrors the new value into its own state immediately. Also fix the init paths to apply the persisted value unconditionally (was only re-applying when true), and bootstrap the body class from localStorage at the _app root so deep-links to /uxcore etc. also pick up the persisted theme. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The bare hr inside OurProjectsModal.module.scss's :global(body.darkTheme)
block was rejected by CSS Modules ("not pure — selectors must contain at
least one local class or id"), failing the DEV build. Move the dark-mode
divider rule into uxcore/styles/globals.scss under body.darkTheme where
a bare element selector is allowed, so every <hr> in dark mode gets the
same divider treatment instead of one modal carrying its own copy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…odal UX Core LogInModal: the Twitter / X provider icon is a single-path SVG hard-coded to fill #000, which disappears against the dark provider button bg. Switch the uxcore copy of XIcon to fill="currentColor" so it inherits the link's text color — light grey in dark mode, black in light mode (matches the old visual). KeepSimple-side LogIn modal: this is a different component (different visual design, different Modal chrome) and was never dark-themed nor bounded to viewport height. Two problems fixed: 1. Position. A tall content tree (six provider buttons + email form) exceeded 100dvh and the centered modal clipped above and below the viewport. Bound the wrapper with max-height: calc(100dvh - 32px) and give the body flex: 1 + min-height: 0 so its overflow: auto engages. 2. Dark theme. Re-skin the wrapper (drop the paper-texture bg image and apply a dark surface), the header copy, the LogIn heading and subtitle, the Google button (gets a faint border so it doesn't blow out against the dark wrapper), the error banner, and the MagicLink email form (divider, label, input, submit, banner, confirmation). Brand-colored provider buttons (Discord / LinkedIn / X / Mail.ru / Yandex) keep their own colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three pieces: 1. Modal scrollbar. The newly-bounded keepsimple modal body was using the default browser scrollbar (wide, white). Apply the same slim thumb treatment used elsewhere on the site so the scroll surface reads as part of the modal chrome instead of an OS widget. 2. Open / close fade transitions for the UX Core Modal and CustomModal. The keepsimple-side Modal already had wrapperIn / wrapperOut + overlayFadeIn / Out animations and a 180ms delayed-unmount close. The UX Core copies had neither, so modals popped in and out. Add the same pattern (closing state, delayed onClose, CSS animations) to both UX Core Modal and CustomModal so every modal site-wide animates in and out instead of snapping. 3. UXCP dark theme. The /uxcp page (UX Core Persona) was light-on-light with the dark page bg, leaving every card and form unreadable. Add dark-mode overrides for the layout headings (UX CORE PERSONA title, subtitle, Motto, section Heading), the shared Section card chrome, CountryBiasMap (search input, country grid cards, selected card, show-more / show-less buttons), BiasPanel (the selected-country detail card, bias chips, help popover, Use button), BiasItem (bias rows in the persona builder), PersonaButton (Copy URL button neutral variant), Switcher (All / High / Medium / Low filter pills), TabHeader (tab card with hover state), and BiasSearch (scrollbar thumb). Brand-colored buttons keep their colors. Also a flag-display fix on the country picker: flag images come from the flagcdn.com external CDN, which is commonly blocked by ad-blockers and some restrictive network policies — that's why every flag row showed a broken-image icon. Add an onError fallback that switches to a Unicode regional-indicator emoji flag (🇦🇷, 🇦🇲 etc.) so the row stays readable when the CDN is unreachable instead of showing a broken-image glyph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. Modal-open scrollbar shift on every page. The previous scrollbar-gutter
fix lived in uxcore/styles/globals.scss — but that file is not imported
anywhere, so the rule was dead code. Move it to src/styles/globals.scss
(the file _app.tsx actually loads) and make it unconditional on <html>.
The page now reserves the scrollbar's gutter at all times, so opening
any modal — which locks scroll by setting overflow-y: hidden on <html>
— no longer reflows the layout sideways.
2. Modal body scrollbars. The keepsimple Modal already got a slim styled
scrollbar; bring the UX Core Modal body and the CustomModal body into
the same shape (6px width, soft blue thumb) with dark-theme variants
(light-blue thumb on dark surface).
3. UXCP dark-theme gaps. Several surfaces were still light-on-light:
- The "Choose your Rival" outer card carried a hard-coded white bg
and dark text. Clear the bg and lift the eyebrow / subtitle / lead
to dark-theme values.
- The CountryMap label text-shadow was hard-coded white (designed to
glow against the light map), which left a white halo on dark.
Flip to a dark glow and dark-theme the hover tooltip.
- The UXCPDescription welcome paragraph and "on Medium via this link."
inline link were hard-coded dark, invisible on dark bg.
- Lift the BiasPanel rationale and bias-chip description from a 0.7
alpha to solid #c8c8c8 — the dim text was hard to read on dark.
- Slim styled scrollbar on the inner bias-chip description (it
overflows on long bias descriptions).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two real-world bugs Wolf hit on DEV that did not reproduce on prod: 1. UXCP appeared light-on-light in dark mode. The body.darkTheme rule was setting only `background-color: #1b1e26`, but the base body rule used `background: linear-gradient(...) #f9fafb` (shorthand: light gradient image PLUS color). The shorthand sets background-image as well, and the dark-theme override only changed the color — the LIGHT gradient kept rendering on top, so anywhere a child surface did not carry its own dark bg (like UXCP), the page leaked back to white. Use the full `background:` shorthand on body.darkTheme so the image is also cleared. 2. Country flags were blocked by CSP, not by an ad-blocker. The site's img-src directive in next.config.js whitelists the strapi hosts + Google + Discord but does NOT include flagcdn.com — so every flag request was refused by the browser with a CSP violation, which is why the same flagcdn URLs that load on prod failed here. Prod must have been built before the CSP tightened; DEV has the latest config. Add https://flagcdn.com to the img-src allowlist. Plus: LogIn modal provider buttons now have proper hover + active + keyboard-focus states (lift on hover, depress on click, brand-color darken on hover for each provider). And bump UXCP "(UXCP)" subtitle contrast from a 0.6-alpha gray to solid #a8a8a8. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wolf hit four remaining light-on-dark surfaces on /uxcp:
1. Bias-row strip ("Add biases to Persona" list). BiasActionCell used a
pale blue bg with a hard-white Add button — both blew out on dark.
Re-skin the row (faint blue tint, light text, dark Add button with
blue-accent hover) and invert the remove-icon.
2. SelectionView right panel. The "John Doe" persona-name header and
the "Add bias to begin" placeholder were hard-coded mid-gray and
washed out on the dark panel. Lift to readable light-on-dark values.
3. PersonaRelatedQuestions / PriorityFilter. The "Relevance level / All
/ High / Medium / Low" filter strip carried a hard white bg, and the
pill buttons were white-on-light-border. Clear the strip bg and flip
the inactive pill to a dark surface (active blue pill is kept). Also
re-skin the question-list zebra striping and the placeholder.
4. Decision Table. Wrapper border, header row bg, alternating row bg,
hover row bg, the gradient text-fade endpoint (was fading cells to
white — now fades to the dark panel), the empty-cell color, the
footer divider, the Save button neutral state, and the error-message
callout were all light-theme leftovers. Plus give the TabHeader pink
variant ("Decision Table example") a dark-mode counterpart so its
wedge doesn't blow out against dark.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five things Wolf flagged on /uxcp dark:
1. Decision Table cells were rendering as blurred gray smudges. The
light-theme rule uses background-clip:text + a gradient that fades
long cells to white at the bottom; my dark counterpart kept the trick
with a dark gradient, but short text ends up entirely inside the fade
band and reads as a blurred rectangle. Disable the clip-text trick on
dark and paint solid #dadada — text is readable, the trade-off is no
bottom-fade on overflow but the cell already truncates with
ellipsis-via-line-clamp so the fade was decorative.
2. Stage-indicator pill ("Released stage" etc.) rendered with a
saturated brand bg and the label looked black. The DynamicButton sets
color:#fff on the button but a downstream cascade was dimming the
inner span. Force #fff on the button + inner Title in dark mode.
3. Pagination active/inactive states read inverted on dark — the
"inactive" white pill looked more selected than the dark-blue
"active" one. Flip the palette: inactive page sits on a dark surface
with light text + faint border (hover lifts to blue accent), active
page is the brighter #5396d3 blue pill. Also pad the click target
and add a 4px gap between pages so it feels less cramped.
4. Suggested questions copy + link colors were leaving body text dim
and the per-question anchor stuck at the dark-blue light-theme color
on dark. Lift section copy to #dadada and links to #7fb3d5.
5. SuggestedQuestions empty state ("no data") was at #bfbfbf — too
bright on dark, distracting. Drop to rgba(218,218,218,0.45).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "Choose your <X>" subtitle on /uxcp cycles a word through several
values (e.g. Rival / Customer / Audience / Persona). The component set
color inline — `#337AB7` for the bold word ("Persona") and `#1A1A2E`
for the rest. The near-black non-bold color was invisible on the dark
page bg, so only "Persona" showed up while every other rotation looked
blank.
Move the colour + font-weight off the inline style and onto two CSS
classes (CyclingWord / CyclingWordBold) so the dark-theme rule can
override: non-bold lifts to #dadada, bold stays a light blue accent
(#7fb3d5). Light theme keeps the original colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two follow-ups on /uxcp dark:
1. Stage-indicator pill ("Released stage" etc.). Previous attempt set
color:#fff !important on .DynamicButton, but the cascade was still
reaching the inner `.Title` text node through body.darkTheme's
inherited #dadada via specificity. Apply the white override to every
child node (.DynamicButton, .Titles, .Title) AND darken the saturated
brand background ~20-25% on each Active state so the white label has
real contrast (the coral and orange variants in particular were
washing the label out at full saturation).
2. Persona-relevant questions rows. RelatedQuestion.Text was hard-coded
#000 and the divider #e9e9e9 — both visible on light, both wrong on
dark. Lift the question text to #dadada (#fff on hover), the bias
number to the #7fb3d5 accent, and the row divider to #303338.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /uxcat route stack had no dark-mode coverage — every visible surface across the four pages (landing, start-test, ongoing, test-result) was light-on-light against the dark page bg. Adds :global(body.darkTheme) blocks to: LAYOUTS - UXCatLayout — page bg clears, title flips to the #7fb3d5 accent, subtitle to a readable mid-gray. - OngoingLayout — question card chrome, question text, bias-info chip, answer rows + answer-prefix tag + selected-state border, skeleton placeholders, and the "btn icon" SVG fill in the footer. - TestResult — section header card, encourage / additionalInfo split panel + center divider, reading-list links, "next test available" date accent. - StartTestLayout — duration badge (desktop + mobile variant) and modal description text. - CalculatingResults — info card, loader spinner border, progress-item track. - CertificateLayout — footer button bar. COMPONENTS - UserProfile — name/title/level/awareness-points/user-statistic color shades, badge tooltip. - Result — passed/failed result cards, question pills, hover state, "failed" row tint, bobIcon container. - CompletionBar — bar surface, mainTitle, level text, base track, step number color, hover tooltip background. - AchievementContainer — grid container surface + mainTitle. - TestResultsAchievements — card surface + achievement title. - UXCatFooter — contact-us copy, mail/telegram links, motto accent. Brand-coloured accents (orange title in StartTest, the level-up gold gradients in CompletionBar, etc.) are left untouched — they read on dark already and changing them would lose intent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wolf hit four remaining surfaces on /uxcat that were still light-on-dark: 1. Rules accordion body — the Accordion's existing dark theme lived under a `&.darkTheme` modifier that only kicks in when the component opts in by setting that class on itself. UXCat does not, so the body stayed white-on-light. Auto-apply via `:global(body.darkTheme)` so every accordion picks up dark without per-component plumbing — title bar, content surface, border, download-button accent. 2. UserProfile cover. The header card sets `background-image` inline from JS pointing at the light pastel coverImage.png (or a Strapi cover). Cover image kept rendering on top of the dark page bg with washed-out "Guest User" text on it. Override with `!important` to drop the image in dark mode and use flat #15181f (guest) / #1b1e26 (logged-in) surfaces. Plus a faint border + radius so the panel reads as a card. 3. Level-progression + achievements hover tooltips. Both used a `.tooltipContainer` class on react-tooltip, which portals the popup to <body>. The previous dark-mode rules were descendant-scoped (`.progressBarWrapper .tooltipContainer`, `.userProfile .badgeTooltip`) so they never matched the portaled DOM. Move to flat `:global(body.darkTheme) .tooltipContainer` rules so the popovers actually flip to the dark surface. 4. "Show all achievements" outline button + every other OrangeOutline / BlueOutline button on dark — the variants kept their white bg. Switch to a transparent surface in dark theme so the brand border + text read on the dark page; keep the brand colors untouched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tip body
Three more dark-mode leaks on /uxcat:
1. Accordion chevron. The collapse/expand caret is rendered from
/assets/icons/caret.svg by default and only swaps to caret-dark.svg
when the parent passes isDarkTheme={true} — UXCatLayout does not.
Result: dark caret on dark title bar, invisible. Invert the icon
via CSS filter inside the body.darkTheme Accordion rule so it shows
white regardless of which sprite shipped.
2. Section titles "Level Progression" / "Achievements" / etc. These
render via the shared UXCatPageTitle component whose .pageTitle
class was hard-coded to rgba(0,0,0,0.65) — invisible on dark.
Lift to #dadada in dark mode.
3. Achievement / level hover tooltip body. The .tooltipContainer
wrapper got a dark surface in the previous pass, but the text
actually inside (UXCatTooltip — title, description, unlocked /
statistics labels, points-to-next-level color) was still rendered
in #000000a6 / #000000d9 / #5e62a7 / #a6a6a6. Lift each to the dark
palette so the popover body reads.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
This is a large, well-structured PR. The dark-mode coverage is thorough and the architectural fixes (router.asPath → router.pathname, uxcgLocalizedData as prop, cross-realm sync) are sound. A few issues worth addressing before merge. Issues1. Duplicate
|
| return ( | ||
| <span | ||
| className={styles.CyclingWord} | ||
| className={`${styles.CyclingWord} ${isBold ? styles.CyclingWordBold : ''}`} |
There was a problem hiding this comment.
We use classnames for these cases.
Per Mary's review: - Remove duplicate @font-face block from src/uxcore/styles/globals.scss (lines 90-258 in the previous state). The same declarations were consolidated into src/styles/globals.scss in 2fa2586; the uxcore-side copy was left behind. The file is not imported anywhere, but the cleanup eliminates the duplicate-declaration risk if it ever becomes loaded. Keep body/html resets, scrollbar styles, dark-theme body rules, and the mobile background-size override. - Fix stale-closure in modal Escape listeners. CustomModal.tsx and the uxcore Modal.tsx registered their keydown handlers in a useEffect whose handleClose captured the initial `closing` state, so a second Escape during the 180ms fade-out could re-enter the close cycle. Read handleClose through a ref so the `if (closing) return` guard always sees the current value. - Merge the two split :global(body.darkTheme) .userProfile blocks in UserProfile.module.scss into one. No runtime change. - Use classnames (`cn`) for the CountryBiasMap CyclingWord className instead of an inline template literal with a ternary. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Dark-mode pass across UX Core (now in this repo post-consolidation), plus the layout/context plumbing fixes that had to land alongside it.
Test plan